package top.fullj.money;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Locale;

/**
 * @author bruce.wu
 * @since 2021/11/11 16:06
 */
public final class Money implements Comparable<Money> {

    private static final String KEY_DEFAULT_CURRENCY_CODE = "top.fullj.money.defaultCurrencyCode";

    private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_EVEN;

    private static final int[] CENTS = {1,10,100,1000};

    public static void setDefaultCurrencyCode(String currencyCode) {
        if (isEmpty(currencyCode)) {
            System.clearProperty(KEY_DEFAULT_CURRENCY_CODE);
        } else {
            System.setProperty(KEY_DEFAULT_CURRENCY_CODE, currencyCode);
        }
    }

    @Nonnull
    public static Money zero() {
        return of(BigDecimal.ZERO, getDefaultCurrency());
    }

    @Nonnull
    public static Money zero(@Nonnull String currencyCode) {
        return zero(Currency.getInstance(currencyCode));
    }

    @Nonnull
    public static Money zero(@Nonnull Currency currency) {
        return of(BigDecimal.ZERO, currency);
    }

    @Nonnull
    public static Money parse(@Nonnull String str) {
        String currencyCode = str.substring(0,3);
        String amount = str.substring(3);
        return of(amount, Currency.getInstance(currencyCode));
    }

    @Nonnull
    public static Money of(@Nullable BigDecimal amount) {
        return of(amount, getDefaultCurrency());
    }

    @Nonnull
    public static Money of(@Nullable BigDecimal amount, @Nonnull String currencyCode) {
        return of(amount, Currency.getInstance(currencyCode));
    }

    @Nonnull
    public static Money of(@Nullable BigDecimal amount, @Nonnull Currency currency) {
        return (amount == null) ? zero(currency)
                : new Money(amount, currency);
    }

    @Nonnull
    public static Money of(@Nullable String amount) {
        return of(amount, getDefaultCurrency());
    }

    @Nonnull
    public static Money of(@Nullable String amount, @Nonnull String currencyCode) {
        return of(amount, Currency.getInstance(currencyCode));
    }

    @Nonnull
    public static Money of(@Nullable String amount, @Nonnull Currency currency) {
        if (isEmpty(amount))
            return zero(currency);
        return of(new BigDecimal(amount), currency);
    }

    @Nonnull
    private static Currency getDefaultCurrency() {
        String currencyCode = System.getProperty(KEY_DEFAULT_CURRENCY_CODE);
        if (isEmpty(currencyCode)) {
            return Currency.getInstance(Locale.getDefault());
        }
        return Currency.getInstance(currencyCode);
    }

    private long amount;
    private final Currency currency;

    private Money(long amount, Currency currency) {
        this.currency = currency;
        this.amount = amount;
    }

    private Money(BigDecimal amount, Currency currency) {
        this.currency = currency;
        this.amount = amount.multiply(BigDecimal.valueOf(centFactor()))
                .setScale(currency.getDefaultFractionDigits(), ROUNDING_MODE)
                .longValue();
    }

    @Nonnull
    public BigDecimal amount() {
        return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits());
    }

    @Nonnull
    public Currency currency() {
        return currency;
    }

    @Nonnull
    public Money add(@Nonnull Money other) {
        checkCurrency(other);
        return new Money(amount + other.amount, currency);
    }

    @Nonnull
    public Money add(@Nullable BigDecimal amt) {
        if (amt == null) return this;
        return new Money(amount().add(amt), currency);
    }

    @Nonnull
    public Money sub(@Nonnull Money other) {
        checkCurrency(other);
        return new Money(amount - other.amount, currency);
    }

    @Nonnull
    public Money sub(@Nullable BigDecimal amt) {
        if (amt == null) return this;
        return new Money(amount().subtract(amt), currency);
    }

    @Nonnull
    public Money mul(@Nonnull String ratio) {
        return mul(new BigDecimal(ratio));
    }

    @Nonnull
    public Money mul(double ratio) {
        return mul(BigDecimal.valueOf(ratio));
    }

    @Nonnull
    public Money mul(long ratio) {
        return mul(BigDecimal.valueOf(ratio));
    }

    @Nonnull
    public Money mul(@Nonnull BigDecimal ratio) {
        return new Money(amount().multiply(ratio), currency);
    }

    @Nonnull
    public Money[] alloc(int n) {
        if (n < 1) {
            throw new IllegalArgumentException("n=" + n);
        }
        Money low = new Money(amount / n, currency);
        Money high = new Money(low.amount + 1, currency);
        Money[] result = new Money[n];
        int remain = (int) (amount % n);
        for (int i = 0; i < remain; i++) result[i] = high;
        for (int i = remain; i < n; i++) result[i] = low;
        return result;
    }

    @Nonnull
    public Money[] alloc(@Nonnull long[] ratios) {
        if (ratios.length < 1) {
            throw new IllegalArgumentException("ratios.length=" + ratios.length);
        }
        long total = 0;
        for (long ratio : ratios) total += ratio;
        long remain = amount;
        Money[] result = new Money[ratios.length];
        for (int i = 0; i < result.length; i++) {
            result[i] = new Money(amount * ratios[i] / total, currency);
            remain -= result[i].amount;
        }
        for (int i = 0; i < remain; i++) {
            result[i].amount++;
        }
        return result;
    }

    @Nonnull
    public Money with(@Nullable BigDecimal amount) {
        return amount == null ? zero(currency)
                : new Money(amount, currency);
    }

    @Nonnull
    public Money with(@Nonnull Operator op) {
        return op.apply(this);
    }

    @Override
    public int compareTo(@Nonnull Money other) {
        checkCurrency(other);
        return Long.compare(amount, other.amount);
    }

    /**
     * Greater than
     */
    public boolean gt(@Nonnull Money other) {
        return compareTo(other) > 0;
    }

    /**
     * Less than
     */
    public boolean lt(@Nonnull Money other) {
        return compareTo(other) < 0;
    }

    public boolean gte(@Nonnull Money other) {
        return !lt(other);
    }

    public boolean lte(@Nonnull Money other) {
        return !gt(other);
    }

    @Override
    public int hashCode() {
        return (int)(amount ^ (amount >>> 32));
    }

    @Override
    public String toString() {
        return format("");
    }

    @Nonnull
    public String format(@Nonnull String separator) {
        return currency.getCurrencyCode() + separator + amount().toPlainString();
    }

    @Override
    public boolean equals(Object other) {
        if (other == null)
            return false;
        return (other instanceof Money) && equals((Money) other);
    }

    public boolean equals(@Nonnull Money other) {
        return currency.equals(other.currency) && (amount == other.amount);
    }

    private int centFactor() {
        int digits = currency.getDefaultFractionDigits();
        assert digits >= 0;
        return CENTS[digits];
    }

    private void checkCurrency(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalStateException("Currency mismatch: " + currency.getCurrencyCode() + "/" + other.currency.getCurrencyCode());
        }
    }

    private static boolean isEmpty(String str) {
        return str == null || str.length() == 0;
    }

    public interface Operator {

        @Nonnull
        Money apply(@Nonnull Money money);

    }

}
